摘要
本篇是初學者在 VS Code 環境簡單測試 Gemini CLI 、 Github Copilot GPT-4.1(預設)並評價兩者對初學者的友善度。
比較項目為:debug、解釋、註解能力、生產 Jest 測試框架能力、連動 VS Code 能力。
下面以詢問debug 與解釋 index.html,再加上註解以及產 Jest 測試
舉例:
#準備好的含 bug index.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>拖延怪日記</title>
<style>
body { font-family: sans-serif; margin: 20px; }
section { display: none; }
section.active { display: block; }
</style>
</head>
<body>
<nav>
<button onclick="show('home')">首頁</button>
<button onclick="show('form')">填寫</button>
<button onclick="show('history')">紀錄</button>
</nav>
<section id="home" class="active">
<h1>歡迎來到拖延怪日記</h1>
</section>
<section id="form">
<h1>今天想做什麼?</h1>
<input id="task" placeholder="輸入任務">
<button onclick="saveTask()">儲存</button>
</section>
<section id="history">
<h1>紀錄</h1>
<ul id="list"></ul>
</section>
<script>
function show(id) {
document.querySelectorAll("section").forEach(s => s.classList.remove("active"));
document.getElementById(id).classList.add("active");
}
function saveTask() {
const task = document.getElementById("task").value;
if (!task) return alert("請輸入任務!");
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
history.unshift(task);
localStorage.setItem("tasks", JSON.stringify(history));
render();
show('history');
}
function render() {
const list = document.getElementById("list");
list.innerHTML = "";
const history = JSON.parse(localStorage.getItem("taskList") || "[]"); // 🐞 Bug: key 寫錯,應該是 "tasks"
history.forEach(t => {
const li = document.createElement("li");
li.textContent = t;
list.appendChild(li);
});
}
render(); // 初始化載入
</script>
</body>
</html>
Gemini CLI 預設會給你看修改前後對照。
詢問你是否這次同意更改,或者可以選後續一律授權 CLI 直接改。
# Gemini 修正並加上註解的 index.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>拖延怪日記</title>
<style>
4 /* 基本頁面樣式 */
body { font-family: sans-serif; margin: 20px; }
/* 預設隱藏所有 section 區塊 */
section { display: none; }
/* 只顯示有 'active' class 的 section 區塊 */
section.active { display: block; }
</style>
</head>
<body>
<!-- 導覽列 -->
<nav>
<button onclick="show('home')">首頁</button>
<button onclick="show('form')">填寫</button>
<button onclick="show('history')">紀錄</button>
</nav>
<!-- 首頁區塊 -->
<section id="home" class="active">
<h1>歡迎來到拖延怪日記</h1>
</section>
<!-- 任務填寫區塊 -->
<section id="form">
<h1>今天想做什麼?</h1>
<input id="task" placeholder="輸入任務">
<button onclick="saveTask()">儲存</button>
</section>
<!-- 歷史紀錄區塊 -->
<section id="history">
<h1>紀錄</h1>
<!-- 任務清單將會顯示在這裡 -->
<ul id="list"></ul>
</section>
<script>
/**
* 顯示指定的 section 區塊,並隱藏其他區塊
* @param {string} id - 要顯示的 section 的 id
*/
function show(id) {
// 移除所有 section 的 'active' class
document.querySelectorAll("section").forEach(s => s.classList.remove("active"));
// 為目標 section 加上 'active' class,讓它顯示出來
document.getElementById(id).classList.add("active");
}
/**
* 儲存新的任務到 localStorage
*/
function saveTask() {
const taskInput = document.getElementById("task");
const task = taskInput.value;
// 如果沒有輸入內容,就跳出提示
if (!task) {
return alert("請輸入任務!");
}
// 從 localStorage 讀取現有的任務清單,如果沒有就建立一個空陣列
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
// 將新任務加到陣列的最前面
history.unshift(task);
// 將更新後的陣列存回 localStorage
localStorage.setItem("tasks", JSON.stringify(history));
// 清空輸入框
taskInput.value = "";
// 重新渲染畫面上的任務清單
render();
// 切換到歷史紀錄頁面
show('history');
}
/**
* 從 localStorage 讀取任務清單並顯示在頁面上
*/
function render() {
const list = document.getElementById("list");
// 清空目前的清單內容
list.innerHTML = "";
// 【錯誤修正】從 localStorage 讀取任務清單,鍵值應為 "tasks" 而非 "taskList"
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
// 遍歷所有任務並建立 <li> 元素加到清單中
history.forEach(t => {
const li = document.createElement("li");
li.textContent = t;
list.appendChild(li);
});
}
// 頁面載入時,立即執行一次 render,以顯示儲存的紀錄
render();
</script>
</body>
</html>
Gemini 的 index.html 檔案解釋:
Gemini 後續執行步驟:
可惜的是生成 Jest 失敗了五次,因缺環境(沒裝 jest-environment-jsdom)、缺編碼物件(TextEncoder/TextDecoder)、缺 DOM(元素是 null)等。
這裡凸顯另一個優點,CLI 可以撤回他所做的調整,初學者就不必花費額外時間研究如何修正。
Gemini CLI 小結
雖然進階問題仍需要一定基礎才能 debug 成功,但不能否認 Gemini CLI 有以下優點:
同樣以詢問debug 與解釋 index.html,再加上註解以及產 Jest 測試
舉例:
# Copilot 自動修正並加上註解的 index.html
<!-- index.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>拖延怪日記</title>
<style>
body { font-family: sans-serif; margin: 20px; }
section { display: none; }
section.active { display: block; }
</style>
</head>
<body>
<nav>
<button onclick="show('home')">首頁</button>
<button onclick="show('form')">填寫</button>
<button onclick="show('history')">紀錄</button>
</nav>
<section id="home" class="active">
<h1>歡迎來到拖延怪日記</h1>
</section>
<section id="form">
<h1>今天想做什麼?</h1>
<input id="task" placeholder="輸入任務">
<button onclick="saveTask()">儲存</button>
</section>
<section id="history">
<h1>紀錄</h1>
<ul id="list"></ul>
</section>
<script>
// 切換顯示的 section
function show(id) {
document.querySelectorAll("section").forEach(s => s.classList.remove("active"));
document.getElementById(id).classList.add("active");
}
// 儲存任務到 localStorage 並更新畫面
function saveTask() {
const task = document.getElementById("task").value;
if (!task) return alert("請輸入任務!");
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
history.unshift(task);
localStorage.setItem("tasks", JSON.stringify(history));
render();
show('history');
}
// 讀取 localStorage 並渲染任務列表
function render() {
const list = document.getElementById("list");
list.innerHTML = "";
const history = JSON.parse(localStorage.getItem("tasks") || "[]"); // 修正 key
history.forEach(t => {
const li = document.createElement("li");
li.textContent = t;
list.appendChild(li);
});
}
render(); // 初始化載入
</script>
</body>
</html>
Copilot 的 index.html 檔案解釋...?
看起來 Copilot 誤解成解釋 debug 的部分而不是 index.html 本身,並一頭栽進生成 Jest 測試框架的部分:
Step-by-step plan:
Debug & explain the bug in index.html:
The bug is in the render() function: it reads from localStorage.getItem("taskList"), but tasks are saved under "tasks".
This causes the history list to not show saved tasks.
Add comments to index.html explaining each function and the bug fix.
Write Jest tests for the main logic (simulate DOM and localStorage).
Test: saving a task adds it to localStorage.
Test: render displays tasks from localStorage.
Test: empty input shows alert.
Test: navigation switches sections.
Since Jest tests can't run directly in the browser, extract the JS logic into testable functions and mock DOM/localStorage in tests.
換成拆開 prompt 下:debug
突然變成不會自動改了,而是讓使用者自己去另外按按鈕套用,好處是可以先看修改前後:
加註解
# Copilot 加上註解的 index.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>拖延怪日記</title>
<style>
body { font-family: sans-serif; margin: 20px; }
section { display: none; }
section.active { display: block; }
</style>
</head>
<body>
<nav>
<button onclick="show('home')">首頁</button>
<button onclick="show('form')">填寫</button>
<button onclick="show('history')">紀錄</button>
</nav>
<section id="home" class="active">
<h1>歡迎來到拖延怪日記</h1>
</section>
<section id="form">
<h1>今天想做什麼?</h1>
<input id="task" placeholder="輸入任務">
<button onclick="saveTask()">儲存</button>
</section>
<section id="history">
<h1>紀錄</h1>
<ul id="list"></ul>
</section>
<script>
// 切換顯示的 section
function show(id) {
document.querySelectorAll("section").forEach(s => s.classList.remove("active"));
document.getElementById(id).classList.add("active");
}
// 儲存任務到 localStorage 並顯示紀錄
function saveTask() {
const task = document.getElementById("task").value;
if (!task) return alert("請輸入任務!"); // 若未輸入則提示
const history = JSON.parse(localStorage.getItem("tasks") || "[]"); // 取得現有任務
history.unshift(task); // 新任務加到最前面
localStorage.setItem("tasks", JSON.stringify(history)); // 存回 localStorage
render(); // 重新渲染紀錄
show('history'); // 跳到紀錄頁
}
// 渲染任務紀錄列表
function render() {
const list = document.getElementById("list");
list.innerHTML = ""; // 清空列表
const history = JSON.parse(localStorage.getItem("tasks") || "[]"); // 取得任務
history.forEach(t => {
const li = document.createElement("li");
li.textContent = t; // 顯示任務內容
list.appendChild(li); // 加到列表
});
}
render(); // 頁面載入時渲染一次
</script>
</body>
</html>
產 Jest 測試
#Copilot 生成的 Jest
/**
* @jest-environment jsdom
*/
describe('拖延怪日記功能測試', () => {
beforeEach(() => {
document.body.innerHTML = `
<section id="form">
<input id="task" />
<button id="saveBtn">儲存</button>
</section>
<section id="history">
<ul id="list"></ul>
</section>
`;
localStorage.clear();
// 模擬 render 與 saveTask
window.render = function() {
const list = document.getElementById("list");
list.innerHTML = "";
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
history.forEach(t => {
const li = document.createElement("li");
li.textContent = t;
list.appendChild(li);
});
};
window.saveTask = function() {
const task = document.getElementById("task").value;
if (!task) return;
const history = JSON.parse(localStorage.getItem("tasks") || "[]");
history.unshift(task);
localStorage.setItem("tasks", JSON.stringify(history));
render();
};
});
test('儲存任務後 localStorage 有資料', () => {
document.getElementById("task").value = "測試任務";
window.saveTask();
const tasks = JSON.parse(localStorage.getItem("tasks"));
expect(tasks[0]).toBe("測試任務");
});
test('render 會顯示所有任務', () => {
localStorage.setItem("tasks", JSON.stringify(["A", "B"]));
window.render();
const items = Array.from(document.querySelectorAll("#list li")).map(li => li.textContent);
expect(items).toEqual(["A", "B"]);
});
test('未輸入任務不會儲存', () => {
document.getElementById("task").value = "";
window.saveTask();
const tasks = JSON.parse(localStorage.getItem("tasks") || "[]");
expect(tasks.length).toBe(0);
});
}); //後注:第一次時 Copilot 忘了生成這個
經過兩次失敗,生成 Jest 測試成功了!遇到的問題是:
不過 Copilot 貌似無法輕易撤回剛剛的所有動作:
Github Copilot GPT-4.1小結
在這次測試來看 Copilot 不太能一次接收過多指令,剛開始使用也會有點呆,但多跟他對話幾次他就能弄清情況,而且擁有以下優點:
Gemini CLI 與 Github Copilot 共同優點:
比較: